#include <logger.h>
#include <napi_call.h>
#include <node_api.h>

struct Data {
    napi_ref promiseRef = nullptr;
    napi_ref resultRef = nullptr;
    napi_deferred deferred = nullptr;
};

void DataFinalize(napi_env env, void *data, void *)
{
    if (data == nullptr) {
        return;
    }

    auto pData = reinterpret_cast<struct Data *>(data);
    if (pData->promiseRef) {
        napi_delete_reference(env, pData->promiseRef);
    }

    if (pData->resultRef) {
        napi_delete_reference(env, pData->resultRef);
    }

    delete pData;
}

struct Data &GetData(napi_env env)
{
    void *ptr = nullptr;
    napi_get_instance_data(env, &ptr);
    if (ptr == nullptr) {
        ptr = new struct Data;
        napi_set_instance_data(env, ptr, DataFinalize, nullptr);
    }
    return *reinterpret_cast<struct Data *>(ptr);
}

napi_value Wait(napi_env env, napi_callback_info info)
{
    auto &data = GetData(env);
    napi_value promise = nullptr;
    NAPI_CALL(env, napi_create_promise(env, &data.deferred, &promise));
    NAPI_CALL(env, napi_create_reference(env, promise, 1, &data.promiseRef));
    return promise;
}

napi_value SetVal(napi_env env, napi_callback_info info)
{
    auto &data = GetData(env);
    size_t argc = 1;
    napi_value argv0 = nullptr;
    napi_get_cb_info(env, info, &argc, &argv0, nullptr, nullptr);
    if (argc == 1) {
        napi_value result;
        NAPI_CALL(env, napi_coerce_to_object(env, argv0, &result));
        NAPI_CALL(env, napi_create_reference(env, result, 1, &data.resultRef));
    }
    return nullptr;
}

napi_value Resolve(napi_env env, napi_callback_info info)
{
    auto &data = GetData(env);
    napi_value result;
    napi_get_reference_value(env, data.resultRef, &result);
    NAPI_CALL(env, napi_resolve_deferred(env, data.deferred, result));
    return nullptr;
}

napi_value Reject(napi_env env, napi_callback_info info)
{
    auto &data = GetData(env);
    napi_value result;
    napi_get_reference_value(env, data.resultRef, &result);
    NAPI_CALL(env, napi_reject_deferred(env, data.deferred, result));
    return nullptr;
}

napi_value IsPromise(napi_env env, napi_callback_info info)
{
    auto &data = GetData(env);
    napi_value promise = nullptr;
    napi_get_reference_value(env, data.promiseRef, &promise);

    bool isPromiseVal = false;
    if (promise != nullptr) {
        NAPI_CALL(env, napi_is_promise(env, promise, &isPromiseVal));
    }

    napi_value isPromise;
    NAPI_CALL(env, napi_get_boolean(env, isPromiseVal, &isPromise));
    return isPromise;
}

/* napi_value */
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */)
{
    auto methodAttrs = static_cast<napi_property_attributes>(
        napi_writable | napi_enumerable | napi_configurable);
    napi_property_descriptor props[] = {
        {.utf8name = "wait", .method = Wait, .attributes = methodAttrs},
        {.utf8name = "set_val", .method = SetVal, .attributes = methodAttrs},
        {.utf8name = "resolve", .method = Resolve, .attributes = methodAttrs},
        {.utf8name = "reject", .method = Reject, .attributes = methodAttrs},
        {.utf8name = "is_promise", .method = IsPromise, .attributes = methodAttrs},
    };
    NAPI_CALL(env, napi_define_properties(env, exports, sizeof(props) / sizeof(*props), props));
    return exports;
}
